Skip to content

feat(plugins): add linear-triage plugin#15

Open
shannonwho wants to merge 2 commits into
mainfrom
feature/linear-triage-plugin
Open

feat(plugins): add linear-triage plugin#15
shannonwho wants to merge 2 commits into
mainfrom
feature/linear-triage-plugin

Conversation

@shannonwho
Copy link
Copy Markdown
Collaborator

Summary

  • New Claude Code plugin at plugins/linear-triage/ that polls a Linear team's triage queue for issues labeled Claude Code and implements each one end-to-end (worktree → draft PR → tests → ready-for-review), posting stage updates back to the issue as comments.
  • Bundles three skills (linear-triage-setup, linear-triage-poller, linear-issue-worker) plus a recommended settings.json permission allowlist so the poller can run unattended via RemoteTrigger cron.
  • Includes a docs/architecture.md diagram of how the skills coordinate with the Linear MCP server, README setup/troubleshooting/permissions sections, and an example config.

Built on the Symphony pattern: tracker-as-queue + isolated worktree per issue + one agent per issue.

Test plan

  • claude --plugin-dir plugins/linear-triage loads the plugin and exposes all three slash commands
  • /linear-triage-setup creates the "Claude Code" label in Linear and registers the */15 * * * * RemoteTrigger
  • Labeling a triage issue with "Claude Code" and running /linear-triage-poller claims the issue (state → In Progress), posts an intake comment, and spawns a worker
  • Worker creates a worktree on the Linear gitBranchName, opens a draft PR auto-linked to the issue, runs testCommand, and flips the issue to In Review on success
  • Failing tests label the issue needs-human and stop without merging
  • With settings.json merged into .claude/settings.local.json, no permission prompts block the background worker

🤖 Generated with Claude Code

Distributable Claude Code plugin that polls the Linear triage queue for
issues labeled "Claude Code" and implements each one end-to-end (worktree,
draft PR, tests, status updates). Bundles three skills:
linear-triage-setup, linear-triage-poller, linear-issue-worker.

Built on the Symphony pattern (https://github.com/openai/symphony):
tracker-as-queue + isolated worktree per issue + agent-per-issue.
Copy link
Copy Markdown

@linear-code linear-code Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments. Two high-severity issues need attention before merging: a race condition in issue claiming that will cause duplicate work under concurrent poller runs, and the Bash allow-all permission in settings.json that enables arbitrary code execution without prompts in unattended mode.

Comment on lines +35 to +38
### 3a. Claim the issue (prevent double-pickup)
Call `mcp__claude_ai_Linear__save_issue` with:
- `id`: the issue identifier (e.g., `"ABC-123"`)
- `state`: value of `statusMap.inProgress` from config
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: claim step does not prevent duplicate pickup across concurrent poller runs

(High) Step 3a moves the issue to In Progress after the poller already fetched the list in Step 2. The query in Step 2 filters by state: "Triage" — if two poller instances run close together (e.g., the 15-minute RemoteTrigger fires while a manual /linear-triage-poller is also running), both will see the same Triage issues and both will claim and spawn workers for them. Linear's API has no compare-and-swap or optimistic locking on state transitions, so the second save_issue simply overwrites the first without error. The result is two worker agents implementing the same issue concurrently, creating duplicate branches, duplicate PRs, and duplicate Linear comments. The fix is to query for issues NOT already in In Progress AND with the label, and to treat a save_issue conflict (state already changed) as a skip signal — but neither safeguard is present.

"mcp__claude_ai_Linear__list_teams",
"mcp__claude_ai_Linear__get_user",
"Bash",
"Read",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bare Bash in the allow-list grants unattended arbitrary command execution

(Critical) settings.json line 14 allows Bash unconditionally. When the user merges this file into .claude/settings.local.json (the documented setup path), every bash command the worker or poller constructs — including the git clone [repoUrl] and [testCommand] expansions that interpolate values from the Linear issue description — runs without a permission prompt. A malicious or misconfigured Linear issue could inject shell commands through gitBranchName, repoUrl, or testCommand. The worker shells out with these values verbatim (Stage 3 git worktree add, Stage 6 [testCommand] 2>&1). The minimum fix is to restrict the Bash allow pattern to specific safe commands (e.g., Bash(git *), Bash(gh *), Bash(npm test)) rather than a wildcard.

Comment on lines +88 to +94

BRANCH="[gitBranchName from issue]"
ISSUE_ID="[issue identifier in lowercase, e.g. abc-123]"

# Create worktree using Linear's branch name
mkdir -p .worktrees
git worktree add ".worktrees/${ISSUE_ID}" -b "${BRANCH}" origin/main
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worker clones into a user-supplied path without validation, enabling path traversal

(High) Stage 3 of the worker unconditionally clones repoUrl into the repo path from config, and creates worktrees at [repo]/.worktrees/[issue-id] where issue-id comes from the Linear API response. If gitBranchName or the issue identifier contains ../ sequences, the worktree will be created outside the intended directory. There is no sanitization or bounds check on these path components anywhere in the skill.

…lt branch

- settings.json: allow Task and Agent so the headless poller can spawn
  per-issue workers via RemoteTrigger without hitting a permission prompt
  that no human is around to approve.
- worker SKILL: replace hardcoded origin/main with default-branch detection
  (git symbolic-ref, falling back to git remote show) so the plugin works
  on repos whose default branch isn't named main.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant